﻿using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace RoslynExtensions
{
    /// <example>new PropertyChangeEventArgs("NotExists")</example>
    [DiagnosticAnalyzer]
    [ExportDiagnosticAnalyzer(DiagnosticId, LanguageNames.CSharp)]
    internal class PropertyChangeEventArgsPropertyNameAnalyzer : ISyntaxNodeAnalyzer<SyntaxKind>
    {
        public const string DiagnosticId = "PropertyChangeEventArgsPropertyName";
        public static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId,
            "Invalid property name",
            "Property with name '{0}' could not be found in the enclosing type",
            "Usage",
            DiagnosticSeverity.Error
        );

        public ImmutableArray<SyntaxKind> SyntaxKindsOfInterest { get { return ImmutableArray.Create(SyntaxKind.ObjectCreationExpression); } }

        public ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }

        public void AnalyzeNode(SyntaxNode node, SemanticModel semanticModel, Action<Diagnostic> addDiagnostic, CancellationToken cancellationToken)
        {
            if ((var expression = node as ObjectCreationExpressionSyntax) != null)
            {
                if (expression.ArgumentList.Arguments.Count != 1)
                    return;

                // find type
                ITypeSymbol type = semanticModel.GetTypeInfo(expression, cancellationToken).Type;

                if (
                    !InheritsFrom(type, semanticModel.Compilation.GetTypeByMetadataName(typeof(System.ComponentModel.PropertyChangedEventArgs).FullName)) &&
                    !InheritsFrom(type, semanticModel.Compilation.GetTypeByMetadataName(typeof(System.ComponentModel.PropertyChangingEventArgs).FullName))
                )
                    return;

                // find argument
                ArgumentSyntax argument = expression.ArgumentList.Arguments[0]; // TODO: Decide to handle constructors of inherited classes

                // get argument name
                Optional<object> constant = semanticModel.GetConstantValue(argument.Expression, cancellationToken);

                if (!constant.HasValue)
                    return;

                string propertyName = constant.Value as string;

                if (propertyName == null)
                {
                    addDiagnostic(Diagnostic.Create(Rule, argument.Expression.GetLocation()));
                    return;
                }

                // find enclosing method
                TypeDeclarationSyntax enclosingDeclaration = argument.FirstAncestorOrSelf<TypeDeclarationSyntax>();

                ITypeSymbol declaringType = semanticModel.GetDeclaredSymbol(enclosingDeclaration, cancellationToken) as ITypeSymbol
                    ?? semanticModel.GetSymbolInfo(enclosingDeclaration, cancellationToken).Symbol as ITypeSymbol
                ;

                if (declaringType != null && !DefinesOrInheritsProperty(declaringType, propertyName))
                    addDiagnostic(Diagnostic.Create(Rule, argument.Expression.GetLocation(), propertyName));
            }
        }

        private static bool DefinesOrInheritsProperty(ITypeSymbol type, string name)
        {
            while (type != null)
            {
                if (type.GetMembers(name).Any(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic && p.Kind == SymbolKind.Property))
                    return true;

                type = type.BaseType;
            }

            return false;
        }

        public bool InheritsFrom(ITypeSymbol type, ITypeSymbol baseType)
        {
            while (type != null)
            {
                if (type.Equals(baseType))
                    return true;

                type = type.BaseType;
            }

            return false;
        }
    }
}